package com.demo.blockchain;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.json.JSONObject;

import com.demo.blockchain.domain.Block;
import com.demo.blockchain.domain.BlockChainData;
import com.demo.blockchain.tool.Encrypt;
import com.demo.blockchain.tool.JacksonUtil;

/**首先创建一个Blockchain类，在构造器中创建了两个主要的集合，一个用于储存区块链，一个用于储存交易列表，
 * 本文中所有核心的主要代码都写在这个类里，方便随时查看，在实际开发则不宜这么做，应该把代码拆分仔细降低耦合度。
 * Blockchain类用来管理区块链，它能存储交易，加入新块等。
 * @author DELL
 *
 */
public class BlockChain {

	// 存储区块链
    private List<Block> chain;
    // 该实例变量用于当前的交易信息列表
    private List<Map<String, Object>> currentTransactions;
    private Set<String> nodes; // 用于存储网络中其他节点的集合，我们用 HashSet 集合来储存节点，这是一种避免出现重复添加节点的简单方法。
    private static BlockChain blockChain = null;

    private BlockChain() {
        // 初始化区块链以及当前的交易信息列表
        chain = new ArrayList<>();
        currentTransactions= new ArrayList<>();
        nodes = new HashSet<String>();

        // 创建创世区块
        newBlock(100, "0");
    }

    // 创建单例对象
    public static BlockChain getInstance() {
        if (blockChain == null) {
            synchronized (BlockChain.class) {
                if (blockChain == null) {
                    blockChain = new BlockChain();
                }
            }
        }
        return blockChain;
    }

    /**
     * 注册节点
     *
     * @param address
     *            节点地址
     * @throws MalformedURLException
     */
    public Set<String> registerNode(String address) throws MalformedURLException {
        URL url = new URL(address);
        String node = url.getHost() + ":" + (url.getPort() == -1 ? url.getDefaultPort() : url.getPort());
        nodes.add(node);
        return nodes;
    }

    /**
     * 在区块链上新建一个区块
     *
     * @param proof
     *            新区块的工作量证明
     * @param previous_hash
     *            上一个区块的hash值
     * @return 返回新建的区块
     */
    public Block newBlock(long proof, String previous_hash) {

    	Block block = new Block();
    	block.setIndex(getChain().size() + 1);
        block.setTimestamp(System.currentTimeMillis());
        block.setTransactions(getCurrentTransactions());
        block.setProof(proof);
        // 如果没有传递上一个区块的hash就计算出区块链中最后一个区块的hash
        block.setPrevious_hash(previous_hash != null ? previous_hash : hash(getChain().get(getChain().size() - 1)));

        // 重置当前的交易信息列表
        setCurrentTransactions(new ArrayList<Map<String, Object>>());

        getChain().add(block);

        return block;
    }

    public List<Block> getChain() {
        return chain;
    }

    public void setChain(List<Block> chain) {
        this.chain = chain;
    }

    public List<Map<String, Object>> getCurrentTransactions() {
        return currentTransactions;
    }

    public void setCurrentTransactions(List<Map<String, Object>> currentTransactions) {
        this.currentTransactions = currentTransactions;
    }

    /**
     * @return 得到区块链中的最后一个区块
     */
    public Block lastBlock() {
        return getChain().get(getChain().size() - 1);
    }

    /**
     * 生成新交易信息，信息将加入到下一个待挖的区块中
     * newTransactions方法向列表中添加一个交易记录，并返回该记录将被添加到的区块 (下一个待挖掘的区块)的索引
     * @param sender 发送方的地址
     * @param recipient 接收方的地址
     * @param amount 交易数量
     * @return 返回存储该交易事务的块的索引
     */
    public int newTransactions(String sender, String recipient, long amount) {

        Map<String, Object> transaction = new HashMap<>();
        transaction.put("sender", sender);
        transaction.put("recipient", recipient);
        transaction.put("amount", amount);

        getCurrentTransactions().add(transaction);

        return lastBlock().getIndex() + 1;
    }

    /**
     * 生成区块的 SHA-256格式的 hash值
     *
     * @param block
     *            区块
     * @return 返回该区块的hash
     */
    public static String hash(Block block) {
        return new Encrypt().getSHA256(new JSONObject(block).toString());
    }

    /**
     * 检查是否是有效链，遍历每个区块验证hash和proof，来确定一个给定的区块链是否有效
     *
     * @param chain
     * @return
     */
    public boolean validChain(List<Block> chain) {
    	Block lastBlock = chain.get(0);
        int currentIndex = 1;
        while (currentIndex < chain.size()) {
        	Block block = chain.get(currentIndex);
            System.out.println(lastBlock.toString());
            System.out.println(block.toString());
            System.out.println("\n-------------------------\n");

            // 检查block的hash是否正确
            if (!block.getPrevious_hash().equals(hash(lastBlock))) {
                return false;
            }

            lastBlock = block;
            currentIndex++;
        }
        return true;
    }

    /**
     * 共识算法解决冲突，使用网络中最长的链. 遍历所有的邻居节点，并用上一个方法检查链的有效性， 如果发现有效更长链，就替换掉自己的链
     *
     * @return 如果链被取代返回true, 否则返回false
     * @throws IOException
     */
    public boolean resolveConflicts() throws IOException {
        Set<String> neighbours = nodes;
        List<Block> newChain = null;

        // 寻找最长的区块链
        long maxLength = chain.size();

        // 获取并验证网络中的所有节点的区块链
        for (String node : neighbours) {

            URL url = new URL("http://" + node + "/chain");
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.connect();

            if (connection.getResponseCode() == 200) {
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "utf-8"));
                StringBuffer responseData = new StringBuffer();
                String response = null;
                while ((response = bufferedReader.readLine()) != null) {
                    responseData.append(response);
                }
                bufferedReader.close();

                BlockChainData blockChainData = JacksonUtil.jsonToObject(responseData.toString(), BlockChainData.class);
                long length = blockChainData.getLength();
                List<Block> chain = blockChainData.getChain();

                // 检查长度是否长，链是否有效
                if (length > maxLength && validChain(chain)) {
                    maxLength = length;
                    newChain = chain;
                }
            }

        }
        // 如果发现一个新的有效链比我们的长，就替换当前的链
        if (newChain != null) {
            this.chain = newChain;
            return true;
        }
        return false;
    }

    public Set<String> getNodes() {
        return nodes;
    }

}
